/*
 * This file is part of the Chelsio T3 Ethernet driver.
 *
 * Copyright (C) 2009 Chelsio Communications.  All rights reserved.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the LICENSE file included in this
 * release for licensing terms and conditions.
 */

#include "common.h"
#include "regs.h"

enum {
	/* MDIO_DEV_PMA_PMD registers */
	AQ_LINK_STAT	= 0xe800,

	/* MDIO_DEV_XGXS registers */
	AQ_XAUI_RX_CFG	= 0xc400,
	AQ_XAUI_KX_CFG	= 0xc440,
	AQ_XAUI_TX_CFG	= 0xe400,

	/* MDIO_DEV_ANEG registers */
	AQ_100M_CTRL	= 0x0010,
	AQ_10G_CTRL	= 0x0020,
	AQ_1G_CTRL	= 0xc400,
	AQ_ANEG_STAT	= 0xc800,

	/* MDIO_DEV_VEND1 registers */
	AQ_FW_VERSION	= 0x0020,
	AQ_THERMAL_THR	= 0xc421,
	AQ_THERMAL1	= 0xc820,
	AQ_THERMAL2	= 0xc821,
	AQ_IFLAG_GLOBAL	= 0xfc00,
	AQ_IMASK_GLOBAL	= 0xff00,
};

#define AQBIT(x)	(1 << (0x##x))
#define ADV_1G_FULL	AQBIT(f)
#define ADV_1G_HALF	AQBIT(e)
#define ADV_10G_FULL	AQBIT(c)

#define AQ_WRITE_REGS(phy, regs) do { \
	int i; \
	for (i = 0; i < ARRAY_SIZE(regs); i++) { \
		(void) mdio_write(phy, regs[i].mmd, regs[i].reg, regs[i].val); \
	} \
} while (0)
#define AQ_READ_REGS(phy, regs) do { \
	unsigned i, v; \
	for (i = 0; i < ARRAY_SIZE(regs); i++) { \
		(void) mdio_read(phy, regs[i].mmd, regs[i].reg, &v); \
	} \
} while (0)

/*
 * Return value is temperature in celcius, 0xffff for error or don't know.
 */
static int
aq100x_temperature(struct cphy *phy)
{
	unsigned int v;

	if (mdio_read(phy, MDIO_DEV_VEND1, AQ_THERMAL2, &v) ||
	    v == 0xffff || (v & 1) != 1)
		return (0xffff);

	if (mdio_read(phy, MDIO_DEV_VEND1, AQ_THERMAL1, &v))
		return (0xffff);

	return ((int)((signed char)(v >> 8)));
}

static int
aq100x_set_defaults(struct cphy *phy)
{
	return mdio_write(phy, MDIO_DEV_VEND1, AQ_THERMAL_THR, 0x6c00);
}

static int
aq100x_reset(struct cphy *phy, int wait)
{
	int err;
	err = t3_phy_reset(phy, MDIO_DEV_PMA_PMD, wait);
	if (!err)
		err = aq100x_set_defaults(phy);
	return (err);
}

static int
aq100x_intr_enable(struct cphy *phy)
{
	struct {
		int mmd;
		int reg;
		int val;
	} imasks[] = {
		{MDIO_DEV_VEND1, 0xd400, AQBIT(e)},
		{MDIO_DEV_VEND1, 0xff01, AQBIT(2)},
		{MDIO_DEV_VEND1, AQ_IMASK_GLOBAL, AQBIT(0)}
	};

	AQ_WRITE_REGS(phy, imasks);

	return (0);
}

static int
aq100x_intr_disable(struct cphy *phy)
{
	struct {
		int mmd;
		int reg;
		int val;
	} imasks[] = {
		{MDIO_DEV_VEND1, 0xd400, 0},
		{MDIO_DEV_VEND1, 0xff01, 0},
		{MDIO_DEV_VEND1, AQ_IMASK_GLOBAL, 0}
	};

	AQ_WRITE_REGS(phy, imasks);

	return (0);
}

static int
aq100x_intr_clear(struct cphy *phy)
{
	struct {
		int mmd;
		int reg;
	} iclr[] = {
		{MDIO_DEV_VEND1, 0xcc00},
		{MDIO_DEV_VEND1, AQ_IMASK_GLOBAL} /* needed? */
	};

	AQ_READ_REGS(phy, iclr);

	return (0);
}

static int
aq100x_vendor_intr(struct cphy *phy, int *rc)
{
	int err;
	unsigned int cause, v;

	err = mdio_read(phy, MDIO_DEV_VEND1, 0xfc01, &cause);
	if (err)
		return (err);

	if (cause & AQBIT(2)) {
		err = mdio_read(phy, MDIO_DEV_VEND1, 0xcc00, &v);
		if (err)
			return (err);

		if (v & AQBIT(e)) {
			CH_WARN(phy->adapter, "PHY%d: temperature is now %dC\n",
			    phy->addr, aq100x_temperature(phy));

			t3_set_reg_field(phy->adapter, A_T3DBG_GPIO_EN,
			    phy->addr ? F_GPIO10_OUT_VAL : F_GPIO6_OUT_VAL, 0);

			*rc |= cphy_cause_alarm;
		}

		cause &= ~4;
	}

	if (cause)
		CH_WARN(phy->adapter, "PHY%d: unhandled vendor interrupt"
		    " (0x%x)\n", phy->addr, cause);

	return (0);

}

static int
aq100x_intr_handler(struct cphy *phy)
{
	int err, rc = 0;
	unsigned int cause;

	err = mdio_read(phy, MDIO_DEV_VEND1, AQ_IFLAG_GLOBAL, &cause);
	if (err)
		return (err);

	if (cause & AQBIT(0)) {
		err = aq100x_vendor_intr(phy, &rc);
		if (err)
			return (err);
		cause &= ~AQBIT(0);
	}

	if (cause)
		CH_WARN(phy->adapter, "PHY%d: unhandled interrupt (0x%x)\n",
		    phy->addr, cause);

	return (rc);
}

static int
aq100x_power_down(struct cphy *phy, int off)
{
	int err, wait = 500;
	unsigned int v;

	err = t3_mdio_change_bits(phy, MDIO_DEV_PMA_PMD, MII_BMCR, BMCR_PDOWN,
	    off ? BMCR_PDOWN : 0);
	if (err || off)
		return (v);

	msleep(300);
	do {
		err = mdio_read(phy, MDIO_DEV_PMA_PMD, MII_BMCR, &v);
		if (err)
			return (err);
		v &= BMCR_RESET;
		if (v)
			msleep(10);
	} while (v && --wait);
	if (v) {
		CH_WARN(phy->adapter, "PHY%d: power-up timed out (0x%x).\n",
		    phy->addr, v);
		return (ETIMEDOUT);
	}

	return (0);
}

static int
aq100x_autoneg_enable(struct cphy *phy)
{
	int err;

	err = aq100x_power_down(phy, 0);
	if (!err)
		err = t3_mdio_change_bits(phy, MDIO_DEV_ANEG, MII_BMCR,
		    BMCR_RESET, BMCR_ANENABLE | BMCR_ANRESTART);

	return (err);
}

static int
aq100x_autoneg_restart(struct cphy *phy)
{
	return aq100x_autoneg_enable(phy);
}

static int
aq100x_advertise(struct cphy *phy, unsigned int advertise_map)
{
	unsigned int adv;
	int err;

	/* 10G advertisement */
	adv = 0;
	if (advertise_map & ADVERTISED_10000baseT_Full)
		adv |= ADV_10G_FULL;
	err = t3_mdio_change_bits(phy, MDIO_DEV_ANEG, AQ_10G_CTRL,
				  ADV_10G_FULL, adv);
	if (err)
		return (err);

	/* 1G advertisement */
	adv = 0;
	if (advertise_map & ADVERTISED_1000baseT_Full)
		adv |= ADV_1G_FULL;
	if (advertise_map & ADVERTISED_1000baseT_Half)
		adv |= ADV_1G_HALF;
	err = t3_mdio_change_bits(phy, MDIO_DEV_ANEG, AQ_1G_CTRL,
				  ADV_1G_FULL | ADV_1G_HALF, adv);
	if (err)
		return (err);

	/* 100M, pause advertisement */
	adv = 0;
	if (advertise_map & ADVERTISED_100baseT_Half)
		adv |= ADVERTISE_100HALF;
	if (advertise_map & ADVERTISED_100baseT_Full)
		adv |= ADVERTISE_100FULL;
	if (advertise_map & ADVERTISED_Pause)
		adv |= ADVERTISE_PAUSE_CAP;
	if (advertise_map & ADVERTISED_Asym_Pause)
		adv |= ADVERTISE_PAUSE_ASYM;
	err = t3_mdio_change_bits(phy, MDIO_DEV_ANEG, AQ_100M_CTRL, 0xfe0, adv);

	return (err);
}

static int
aq100x_set_loopback(struct cphy *phy, int mmd, int dir, int enable)
{
	return t3_mdio_change_bits(phy, MDIO_DEV_PMA_PMD, MII_BMCR,
				   BMCR_LOOPBACK, enable ? BMCR_LOOPBACK : 0);
}

static int
aq100x_set_speed_duplex(struct cphy *phy, int speed, int duplex)
{
	int err, set;

	if (speed == SPEED_100)
		set = BMCR_SPEED100;
	else if (speed == SPEED_1000)
		set = BMCR_SPEED1000;
	else if (speed == SPEED_10000)
		set = BMCR_SPEED1000 | BMCR_SPEED100;
	else
		return (EINVAL);

	if (duplex != DUPLEX_FULL)
		return (EINVAL);

	err = t3_mdio_change_bits(phy, MDIO_DEV_ANEG, MII_BMCR,
	    BMCR_RESET | BMCR_ANENABLE | BMCR_ANRESTART, 0);
	if (err)
		return (err);

	err = t3_mdio_change_bits(phy, MDIO_DEV_PMA_PMD, MII_BMCR,
	    BMCR_SPEED1000 | BMCR_SPEED100, set);
	if (err)
		return (err);

	return (0);
}

static int
aq100x_get_link_status(struct cphy *phy, int *link_ok, int *speed, int *duplex,
		       int *fc)
{
	int err;
	unsigned int v, link = 0;

	err = mdio_read(phy, MDIO_DEV_PMA_PMD, AQ_LINK_STAT, &v);
	if (err)
		return (err);
	if (v == 0xffff || !(v & 1))
		goto done;

	err = mdio_read(phy, MDIO_DEV_ANEG, MII_BMCR, &v);
	if (err)
		return (err);
	if (v & 0x8000)
		goto done;
	if (v & BMCR_ANENABLE) {

		err = mdio_read(phy, MDIO_DEV_ANEG, 1, &v);
		if (err)
			return (err);
		if ((v & 0x20) == 0)
			goto done;

		err = mdio_read(phy, MDIO_DEV_ANEG, AQ_ANEG_STAT, &v);
		if (err)
			return (err);

		if (speed) {
			switch (v & 0x6) {
			case 0x6: *speed = SPEED_10000;
				break;
			case 0x4: *speed = SPEED_1000;
				break;
			case 0x2: *speed = SPEED_100;
				break;
			case 0x0: *speed = SPEED_10;
				break;
			}
		}

		if (duplex)
			*duplex = v & 1 ? DUPLEX_FULL : DUPLEX_HALF;

		if (fc) {
			unsigned int lpa, adv;
			err = mdio_read(phy, MDIO_DEV_ANEG, 0x13, &lpa);
			if (!err)
				err = mdio_read(phy, MDIO_DEV_ANEG,
				    AQ_100M_CTRL, &adv);
			if (err)
				return err;

			if (lpa & adv & ADVERTISE_PAUSE_CAP)
				*fc = PAUSE_RX | PAUSE_TX;
			else if (lpa & ADVERTISE_PAUSE_CAP &&
			    lpa & ADVERTISE_PAUSE_ASYM &&
			    adv & ADVERTISE_PAUSE_ASYM)
				*fc = PAUSE_TX;
			else if (lpa & ADVERTISE_PAUSE_ASYM &&
			    adv & ADVERTISE_PAUSE_CAP)
				*fc = PAUSE_RX;
			else
				*fc = 0;
		}

	} else {
		err = mdio_read(phy, MDIO_DEV_PMA_PMD, MII_BMCR, &v);
		if (err)
			return (err);

		v &= BMCR_SPEED1000 | BMCR_SPEED100;
		if (speed) {
			if (v == (BMCR_SPEED1000 | BMCR_SPEED100))
				*speed = SPEED_10000;
			else if (v == BMCR_SPEED1000)
				*speed = SPEED_1000;
			else if (v == BMCR_SPEED100)
				*speed = SPEED_100;
			else
				*speed = SPEED_10;
		}

		if (duplex)
			*duplex = DUPLEX_FULL;
	}

	link = 1;
done:
	if (link_ok)
		*link_ok = link;
	return (0);
}

static struct cphy_ops aq100x_ops = {
	.reset             = aq100x_reset,
	.intr_enable       = aq100x_intr_enable,
	.intr_disable      = aq100x_intr_disable,
	.intr_clear        = aq100x_intr_clear,
	.intr_handler      = aq100x_intr_handler,
	.autoneg_enable    = aq100x_autoneg_enable,
	.autoneg_restart   = aq100x_autoneg_restart,
	.advertise         = aq100x_advertise,
	.set_loopback      = aq100x_set_loopback,
	.set_speed_duplex  = aq100x_set_speed_duplex,
	.get_link_status   = aq100x_get_link_status,
	.power_down        = aq100x_power_down,
};

int
t3_aq100x_phy_prep(pinfo_t *pinfo, int phy_addr,
		       const struct mdio_ops *mdio_ops)
{
	struct cphy *phy = &pinfo->phy;
	unsigned int v, v2, gpio, wait;
	int err;
	adapter_t *adapter = pinfo->adapter;

	cphy_init(&pinfo->phy, adapter, pinfo, phy_addr, &aq100x_ops, mdio_ops,
		  SUPPORTED_1000baseT_Full | SUPPORTED_10000baseT_Full |
		  SUPPORTED_TP | SUPPORTED_Autoneg | SUPPORTED_AUI |
		  SUPPORTED_MISC_IRQ, "1000/10GBASE-T");

	/*
	 * Hard reset the PHY.
	 */
	gpio = phy_addr ? F_GPIO10_OUT_VAL : F_GPIO6_OUT_VAL;
	t3_set_reg_field(adapter, A_T3DBG_GPIO_EN, gpio, 0);
	msleep(1);
	t3_set_reg_field(adapter, A_T3DBG_GPIO_EN, gpio, gpio);

	/*
	 * Give it enough time to load the firmware and get ready for mdio.
	 */
	msleep(1000);
	wait = 500; /* in 10ms increments */
	do {
		err = mdio_read(phy, MDIO_DEV_PMA_PMD, MII_BMCR, &v);
		if (err || v == 0xffff) {

			/* Allow prep_adapter to succeed when ffff is read */

			CH_WARN(adapter, "PHY%d: reset failed (0x%x, 0x%x).\n",
				phy_addr, err, v);
			goto done;
		}

		v &= BMCR_RESET;
		if (v)
			msleep(10);
	} while (v && --wait);
	if (v) {
		CH_WARN(adapter, "PHY%d: reset timed out (0x%x).\n",
			phy_addr, v);

		goto done; /* let prep_adapter succeed */
	}

	/* Firmware version check. */
	(void) mdio_read(phy, MDIO_DEV_VEND1, AQ_FW_VERSION, &v);
	if (v < 0x115)
		CH_WARN(adapter, "PHY%d: unknown firmware %d.%d\n", phy_addr,
		    v >> 8, v & 0xff);

	/* The PHY should start in really-low-power mode. */
	(void) mdio_read(phy, MDIO_DEV_PMA_PMD, MII_BMCR, &v);
	if ((v & BMCR_PDOWN) == 0)
		CH_WARN(adapter, "PHY%d does not start in low power mode.\n",
			phy_addr);

	/*
	 * Verify XAUI and 1000-X settings, but let prep succeed no matter what.
	 */
	v = v2 = 0;
	(void) mdio_read(phy, MDIO_DEV_XGXS, AQ_XAUI_RX_CFG, &v);
	(void) mdio_read(phy, MDIO_DEV_XGXS, AQ_XAUI_TX_CFG, &v2);
	if (v != 0x1b || v2 != 0x1b)
		CH_WARN(adapter, "PHY%d: incorrect XAUI settings "
		    "(0x%x, 0x%x).\n", phy_addr, v, v2);
	v = 0;
	(void) mdio_read(phy, MDIO_DEV_XGXS, AQ_XAUI_KX_CFG, &v);
	if ((v & 0xf) != 0xf)
		CH_WARN(adapter, "PHY%d: incorrect 1000-X settings "
		    "(0x%x).\n", phy_addr, v);

	(void) aq100x_set_defaults(phy);
done:
	return (err);
}
